(ns rete.node
  (:require [rete.core :refer :all])
  (:import [rete.core Environment PredicateNode ImplicationNode Graph MutablePredicateNode CountingPredicateNode]))

(defn- instersection-of-collection-elements [coll]
  (if (= 1 (count coll)) 
    (first coll)
    (reduce #(clojure.set/intersection %1 (if(set? %2) %2 (set %2)))
            (if (set? (first coll)) 
              (first coll) 
              (set (first coll)))
            (rest coll))))

(defn- get-intersection-of [searched-key nodes]
  (instersection-of-collection-elements  
          (map #(keys (searched-key %1)) nodes )))
  
(defn- update-object-node-relationship [^Environment env node-name object-hash truth-value]
  (update-in 
    (update-in env [:graph :nodes node-name :complied-objects] 
               assoc object-hash {:truth-value truth-value})
    [:working-memory object-hash :nodes] assoc node-name truth-value ))

(defn- remove-object-node-relationship [^Environment env node-name object-hash]
  (update-in 
    (update-in env [:graph :nodes node-name :complied-objects] 
               dissoc object-hash)
    [:working-memory object-hash :nodes ] dissoc node-name))

(defn- update-relationships [^Environment env node-name truth-value]
 (reduce #(update-object-node-relationship %1 node-name %2 truth-value ) 
         env 
         (get-intersection-of :complied-objects
                              (get-nodes (:graph env) 
                                   (get-parents-for-node (:graph env) node-name)))) )

(defn- calculates-truth-values [node objects ^Environment env]
  (map #(assoc %1 (hash %2) (truth-value? node %2 env) )))

(defn- get-to-removed-objects [node objects ^Environment env]
  (map key (filter #(> threshold (val %1)) (calculates-truth-values node objects env))))

;; Definition of needed Protocols
(def predicate-node-protocol {
                              :get-name (fn [this] (:name this))
                              :get-predicate (fn [this] (:predicate this))
                              :truth-value? (fn[this object env] (transform-into-truth-value ((:predicate this) object env)))
                              :add-parent (fn [this parent-name] (update-in this [:parents] conj parent-name))
                              :add-child (fn [this child-name] (update-in this [:children] conj child-name))
                              :remove-parent (fn [this parent-name] (update-in this [:parents] disj parent-name))
                              :remove-child (fn [this child-name] (update-in this [:children] disj child-name))
                              
                              :add-object (fn [this object env] (let[truth-value (truth-value? this object env)] 
                                                                  (if (<= threshold truth-value) 
                                                                    (reduce #(add-object %2 object %1) 
                                                                            (update-object-node-relationship env (get-name this) (hash object) truth-value)
                                                                            (get-nodes (:graph env) (:children this)))
                                                                    env)))
                              
                              :remove-object (fn [this object env] (remove-object-node-relationship env (get-name this) (hash object)))
                              :reevaluate-objects (fn [this objects env] (let [to-removed-object-hashes (get-to-removed-objects this objects env)
                                                                               modified-environment (reduce 
                                                                                                       #(remove-object-node-relationship 
                                                                                                          %1 
                                                                                                          (get-name this)
                                                                                                          %2) 
                                                                                                       env to-removed-object-hashes)
                                                                                to-removed-objects (get-objects modified-environment to-removed-object-hashes)]
                                                                           (reduce #(reevaluate-objects %2 to-removed-objects %1) 
                                                                                   modified-environment
                                                                                   (get-nodes 
                                                                                     (:graph modified-environment)
                                                                                     (:children this)))))
                              
                              })

(def counting-predicate-node-protocol {
                                       ;; Because this node is for counting elements in the parent node, all complied objects in the parent nodes must be considered.
                                       :add-object (fn [this object env] (let [ truth-value (truth-value? this object env)]
                                                                           (if (<= threshold truth-value)
                                                                             (reduce #(add-object %2 object %1) 
                                                                                     (update-relationships env (get-name this) truth-value)
                                                                                     (get-nodes (:graph env) (:children this))))
                                                                           env))
                                       
                                       ;; If an object is removed, then the result of the predicate of this counting node might have been chnaged.
                                       :remove-object (fn [this object env] (let[modified-environment (remove-object-node-relationship env (get-name this) (hash object))
                                                                                 objects (:complied-objects (get-node (:graph modified-environment) (get-name this)))
                                                                                 truth-value (truth-value? this object modified-environment)]
                                                                              (if (<= truth-value threshold) 
                                                                                modified-environment
                                                                                (reevaluate-objects modified-environment 
                                                                                                    (get-objects modified-environment objects) 
                                                                                                    (get-name this)))))
                                       })

(def implication-node-protocol {:change-environment (fn [this ^Environment env] ((:implication this) env))})

(extend PredicateNode Node predicate-node-protocol)
(extend CountingPredicateNode Node (merge predicate-node-protocol counting-predicate-node-protocol))
(extend ImplicationNode Node predicate-node-protocol EnvironmentChange implication-node-protocol)

;;
(defn β-node-condition 
  [object ^Environment env node-name]
  (reduce  #(min %1 (:truth-value 
                      (get (:complied-objects %2) 
                           (hash object) 
                           {:truth-value 0}))) 
           1
           (get-nodes (:graph env) 
                                 (get-parents-for-node (:graph env) node-name))))

(defn action-node-condition 
  [object ^Environment env node-name]
  (reduce  #(max %1 (:truth-value 
                      (get (:complied-objects %2) 
                           (hash object) 
                           {:truth-value 0}))) 
           0
           (get-nodes (:graph env) (get-parents-for-node (:graph env) node-name ))))

(defn new-β-node-condition 
  "(new-β-node-condition name) creates a new condition for a β node, which verifies whether a specifiec object is contained in the fulfilled collection of all the parent nodes." 
  [name] 
  (fn[object ^Environment env](β-node-condition object env name)))

(defn new-implication-node-condition 
  "(new-implication-node-condition name) creates a new condition for an implication node, which verifies whether an object is contained in the fulfilled collection of any the parent nodes." 
  [name] 
  (fn[object ^Environment env](action-node-condition  object env name)))

(defn new-α-node[name condition]
  "(new-α-node name condition) creates a new α-node, with the specified name and condition."
  (PredicateNode. name α-node #{} #{} {} condition))

(defn new-counting-α-node[name condition]
  "(new-counting-α-node name condition) creates a new α-node, with the specified name and counting condition."
  (CountingPredicateNode. name α-node #{} #{} {} condition))

(defn new-implication-node
  ([name environment-change]
    "(new-implication-node name condition) creates a new implication-node with the specified name and change function."
    (ImplicationNode. name implication-node #{} #{} {} (new-implication-node-condition name) environment-change))
  ([name environment-change priority-value]
    {:pre [(number? priority-value) (fn? environment-change)]}
    "(new-implication-node name condition priority-value) creates a new implication-node with the specified name, change function and specified priority."
    (assoc 
      (ImplicationNode. name implication-node #{} #{} {} (new-implication-node-condition name) environment-change)
      priority priority-value
    )))

(defn new-β-node
  ([name]
  "(new-β-node name) creates a new β-node with the specified name."
  (PredicateNode. name β-node #{} #{} {} (new-β-node-condition name)))
  ([]
  "(new-β-node) creates a new β-node with a name based on the time of its creation in nanoseconds."
  (new-β-node (str "β-node #" (hash (str (java.lang.System/nanoTime)))))))
